Skip to content

29 importlib动态导入

有时候你需要根据配置文件、用户输入或者数据库里的字符串来导入模块——比如插件系统加载插件、框架按需加载组件。这时候import语句就不够用了,importlib模块让你在运行时动态导入模块。

一、import_module():动态导入

1.1 基本用法

python
import importlib

# 动态导入模块
itertools = importlib.import_module('itertools')
print(list(itertools.chain([1, 2], [3, 4])))  # [1, 2, 3, 4]

# 动态导入子模块
email_parser = importlib.import_module('email.parser')

1.2 相对导入

python
import importlib

# 相对导入需要指定package参数
# 假设在pkg.subpkg中运行
# import_module('..mod', 'pkg.subpkg') 会导入 pkg.mod

1.3 与__import__()的区别

python
import importlib

# import_module()返回指定的模块
mod = importlib.import_module('email.parser')
print(type(mod))  # <class 'module'>  -- 就是email.parser模块

# __import__()返回顶层包
mod = __import__('email.parser')
print(type(mod))  # <class 'module'>  -- 是email模块,不是email.parser
print(hasattr(mod, 'parser'))  # True

二、检查模块是否存在

2.1 find_spec()

python
import importlib.util

def check_module(name):
    """检查模块是否存在,不实际导入"""
    spec = importlib.util.find_spec(name)
    if spec is None:
        print(f"模块 {name} 不存在")
    else:
        print(f"模块 {name} 存在,位置: {spec.origin}")

check_module('json')      # 存在
check_module('nonexistent')  # 不存在

2.2 安全导入

python
import importlib
import importlib.util
import sys

def safe_import(name):
    """安全导入模块,不存在则返回None"""
    if name in sys.modules:
        return sys.modules[name]
    
    spec = importlib.util.find_spec(name)
    if spec is None:
        return None
    
    return importlib.import_module(name)

json_module = safe_import('json')
if json_module:
    print("json模块可用")

三、reload():重新加载模块

3.1 基本用法

python
import importlib
import my_module  # 假设有个自定义模块

# 修改了my_module.py后,重新加载
importlib.reload(my_module)

3.2 开发时热重载

python
import importlib
import sys

def reload_module(module_name):
    """重新加载指定模块"""
    if module_name in sys.modules:
        module = sys.modules[module_name]
        importlib.reload(module)
        print(f"重新加载 {module_name}")
    else:
        print(f"模块 {module_name} 未加载")

# 使用
reload_module('my_plugin')

3.3 reload的注意事项

python
import importlib

# 注意:reload不会更新已有实例的引用
# 比如:
# from my_module import MyClass
# obj = MyClass()
# importlib.reload(my_module)  # obj仍然用旧的MyClass

# 推荐使用模块名引用
import my_module
obj = my_module.MyClass()
importlib.reload(my_module)  # 现在my_module.MyClass是新的

四、invalidate_caches():刷新缓存

python
import importlib

# 如果在程序运行时安装了新模块,需要刷新缓存
importlib.invalidate_caches()

# 现在可以导入新安装的模块
new_module = importlib.import_module('newly_installed')

五、从文件路径导入

5.1 spec_from_file_location()

python
import importlib.util
import sys

def import_from_path(module_name, file_path):
    """从指定文件路径导入模块"""
    spec = importlib.util.spec_from_file_location(module_name, file_path)
    if spec is None:
        raise ImportError(f"无法从 {file_path} 加载模块")
    
    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module
    spec.loader.exec_module(module)
    return module

# 使用
# my_module = import_from_path('my_module', '/path/to/my_module.py')

5.2 加载配置文件

python
import importlib.util
import sys

def load_config(config_path):
    """把Python文件当配置加载"""
    spec = importlib.util.spec_from_file_location('config', config_path)
    config = importlib.util.module_from_spec(spec)
    sys.modules['config'] = config
    spec.loader.exec_module(config)
    return config

# config.py内容:
# DEBUG = True
# DATABASE_URL = "sqlite:///db.sqlite"

# config = load_config('config.py')
# print(config.DEBUG)  # True

六、延迟导入

6.1 LazyLoader

python
import importlib.util
import sys

def lazy_import(name):
    """延迟导入,第一次访问属性时才真正加载"""
    spec = importlib.util.find_spec(name)
    loader = importlib.util.LazyLoader(spec.loader)
    spec.loader = loader
    module = importlib.util.module_from_spec(spec)
    sys.modules[name] = module
    loader.exec_module(module)
    return module

# 模块对象已创建,但代码未执行
lazy_typing = lazy_import("typing")

# 第一次访问时才真正加载
print(lazy_typing.TYPE_CHECKING)  # 此时才执行typing模块的代码

6.2 按需导入

python
import importlib

# 简单的按需导入模式
def get_module(name):
    """按需导入,已加载则直接返回"""
    import sys
    if name in sys.modules:
        return sys.modules[name]
    return importlib.import_module(name)

# 用到时才导入
data = get_module('json').dumps({"key": "value"})

七、插件系统

7.1 基本插件框架

python
import importlib
import importlib.util
from pathlib import Path

class PluginManager:
    def __init__(self, plugin_dir):
        self.plugin_dir = Path(plugin_dir)
        self.plugins = {}
    
    def discover_plugins(self):
        """发现插件目录下的所有模块"""
        for py_file in self.plugin_dir.glob("*.py"):
            if py_file.name.startswith("_"):
                continue
            module_name = py_file.stem
            spec = importlib.util.spec_from_file_location(
                module_name, py_file
            )
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)
            
            if hasattr(module, 'register'):
                self.plugins[module_name] = module.register()
    
    def get_plugin(self, name):
        return self.plugins.get(name)

# 使用
# pm = PluginManager("./plugins")
# pm.discover_plugins()

7.2 带钩子的插件系统

python
import importlib
from abc import ABC, abstractmethod

class Plugin(ABC):
    @abstractmethod
    def name(self):
        pass
    
    @abstractmethod
    def execute(self, data):
        pass

class PluginLoader:
    def __init__(self):
        self.plugins = {}
    
    def register(self, plugin_class):
        """注册插件类"""
        plugin = plugin_class()
        self.plugins[plugin.name()] = plugin
    
    def load_from_module(self, module_name):
        """从模块加载插件"""
        module = importlib.import_module(module_name)
        for attr_name in dir(module):
            attr = getattr(module, attr_name)
            if (isinstance(attr, type) and 
                issubclass(attr, Plugin) and 
                attr is not Plugin):
                self.register(attr)
    
    def execute(self, plugin_name, data):
        plugin = self.plugins.get(plugin_name)
        if plugin:
            return plugin.execute(data)
        raise ValueError(f"插件 {plugin_name} 不存在")

八、实用工具函数

8.1 获取模块信息

python
import importlib.util

def get_module_info(name):
    """获取模块的详细信息"""
    spec = importlib.util.find_spec(name)
    if spec is None:
        return None
    
    return {
        'name': name,
        'origin': spec.origin,
        'submodule_search_locations': spec.submodule_search_locations,
        'cached': spec.cached,
        'has_location': spec.has_location,
    }

info = get_module_info('json')
print(info)

8.2 列出已加载的模块

python
import sys

def list_loaded_modules():
    """列出所有已加载的模块"""
    for name, module in sorted(sys.modules.items()):
        if hasattr(module, '__file__'):
            print(f"{name}: {module.__file__}")

九、总结

importlib的核心:

函数用途
import_module()动态导入模块
reload()重新加载已导入的模块
invalidate_caches()刷新查找器缓存
find_spec()查找模块规格,不实际导入
spec_from_file_location()从文件路径创建模块规格
module_from_spec()从规格创建模块对象
LazyLoader延迟加载模块

使用场景:

  • 插件系统
  • 按需加载模块
  • 热重载代码
  • 从文件路径加载模块
  • 检查模块是否存在

import_module()是最常用的,记住它和__import__()的区别:前者返回目标模块,后者返回顶层包。